package com.katesoft.scale4j.persistent.spring;

import static com.katesoft.scale4j.persistent.spring.HibernateSessionUtility.hibernateProperties;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.NotWritablePropertyException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.DatabasePopulator;
import org.springframework.orm.hibernate3.HibernateTransactionManager;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;

import com.katesoft.scale4j.common.lock.IDistributedLockProvider;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.hibernate.ICommonHibernateConfiguration;
import com.katesoft.scale4j.persistent.hibernate.StoreUpdateScriptsConfiguration;

/**
 * @author kate2007
 */
@SuppressWarnings("rawtypes")
public class EntityManagedOrNativeHibernateFactoryBean implements FactoryBean<Object>,
         InitializingBean, ILocalHibernateSessionBuilder, ICommonHibernateConfiguration {
   private final Logger log = LogFactory.getLogger(getClass());
   //
   private boolean useJpaInsteadOfNativeHibernate = true;
   private ILocalHibernateSessionBuilder object;
   //
   private final List<Class> annotatedClasses = new LinkedList<Class>();
   private final List configLocations = new LinkedList();
   private List resources = new LinkedList();
   private DatabasePopulator databasePopulator;
   private IDistributedLockProvider lockProvider;
   private Configuration hibernateConfiguration;
   private Map<String, Object> eventListeners;
   private boolean schemaUpdate;
   private StoreUpdateScriptsConfiguration updateScriptsConfiguration = new StoreUpdateScriptsConfiguration();
   private Properties hibernateProperties;
   private DataSource datasource;
   //
   private Map<String, Object> beanProperties = new LinkedHashMap<String, Object>();
   private Object cacheRegionFactory;

   @Override
   public Object getObject() {
      if (object == null) {
         try {
            afterPropertiesSet();
         } catch (Exception e) {
            throw new BeanCreationException(e.getMessage(), e);
         }
      }
      return useJpaInsteadOfNativeHibernate ? ((LocalAnnotationEntityManagerFactory) object)
               .getObject() : ((LocalAnnotationSessionFactory) object).getObject();
   }

   /**
    * create new platform transaction manager according to useJpa attribute.
    * 
    * @param object
    *           session factory holder
    * @return new transaction manager
    * @throws Exception
    *            if getObject() throws exception
    */
   public static PlatformTransactionManager createTransactionManager(Object object)
            throws Exception {
      AbstractPlatformTransactionManager platformTransactionManager;
      if (object instanceof EntityManagerFactory) {
         platformTransactionManager = new JpaTransactionManager((EntityManagerFactory) object);
      } else {
         platformTransactionManager = new HibernateTransactionManager((SessionFactory) object);
      }
      platformTransactionManager.setNestedTransactionAllowed(true);
      return platformTransactionManager;
   }

   public ILocalHibernateSessionBuilder getActualFactoryBuilderBean() {
      return object;
   }

   @Override
   public Class<?> getObjectType() {
      return ILocalHibernateSessionBuilder.class;
   }

   @Override
   public boolean isSingleton() {
      return true;
   }

   @SuppressWarnings({ "unchecked" })
   @Override
   public synchronized void afterPropertiesSet() throws Exception {
      if (useJpaInsteadOfNativeHibernate) {
         object = new LocalAnnotationEntityManagerFactory();
         LocalAnnotationEntityManagerFactory ref = (LocalAnnotationEntityManagerFactory) object;
         ref.setAnnotatedClasses(annotatedClasses);
         ref.setConfigLocations(configLocations);
         ref.setMappingResources(resources);
         ref.setPersistenceUnitName("scale4j.persistent.unit");
      } else {
         object = new LocalAnnotationSessionFactory();
         LocalAnnotationSessionFactory ref = (LocalAnnotationSessionFactory) object;
         ref.setAnnotatedClasses(annotatedClasses.toArray(new Class[annotatedClasses.size()]));
         ref.setConfigLocations((Resource[]) configLocations.toArray(new Resource[configLocations
                  .size()]));
         ref.setMappingResources((String[]) resources.toArray(new String[resources.size()]));
      }
      log.info("[useJpaInsteadOfNativeHibernate=%s] - building new object=%s",
               useJpaInsteadOfNativeHibernate, object);
      object.setDataSource(datasource);
      object.setEventListeners(eventListeners);
      object.setDatabasePopulator(databasePopulator);
      object.setSchemaUpdate(schemaUpdate);
      object.setLockProvider(lockProvider);
      object.setUpdateScriptsConfiguration(updateScriptsConfiguration);
      object.setHibernateProperties(hibernateProperties);
      object.setCacheRegionFactory(cacheRegionFactory);
      // override any bean properties
      BeanWrapperImpl beanWrapper = new BeanWrapperImpl(object);
      for (Entry<String, Object> entry : beanProperties.entrySet()) {
         try {
            beanWrapper.setPropertyValue(entry.getKey(), entry.getValue());
            log.info("injected property[key=%s, value=%s]", entry.getKey(), entry.getValue());
         } catch (NotWritablePropertyException e) {
            log.info("ignoring property %s as this property is not writable", entry.getKey());
         }
      }
      object.afterPropertiesSet();
      hibernateConfiguration = object.getConfiguration();
   }

   /**
    * allows specify whether use jpa entity manager or native hibernate session factory.
    * <p/>
    * By default JPA will be used.
    * 
    * @param useJpaInsteadOfNativeHibernate
    *           jpa or native hibernate
    */
   public void setUseJpaInsteadOfNativeHibernate(boolean useJpaInsteadOfNativeHibernate) {
      this.useJpaInsteadOfNativeHibernate = useJpaInsteadOfNativeHibernate;
   }

   @Override
   public void destroy() throws Exception {
      object.destroy();
   }

   @Override
   public void setMappingResources(List<?> resources) {
      this.resources = resources;
   }

   @Override
   public void setAnnotatedClasses(List<Class> list) {
      this.annotatedClasses.addAll(list);
   }

   @SuppressWarnings({ "unchecked" })
   @Override
   public void setConfigLocations(List<?> configLocations) {
      this.configLocations.addAll(configLocations);
   }

   @Override
   public void updateDatabaseSchema() {
      object.updateDatabaseSchema();
   }

   @Override
   public void setDatabasePopulator(DatabasePopulator databasePopulator) {
      this.databasePopulator = databasePopulator;
   }

   @Override
   public void setLockProvider(IDistributedLockProvider lockProvider) {
      this.lockProvider = lockProvider;
   }

   @Override
   public void setHibernateProperties(Properties properties) {
      this.hibernateProperties = hibernateProperties(properties);
   }

   @Override
   public Configuration getConfiguration() {
      return hibernateConfiguration;
   }

   @Override
   public void setSchemaUpdate(boolean schemaUpdate) {
      this.schemaUpdate = schemaUpdate;
   }

   @Override
   public void setUpdateScriptsConfiguration(StoreUpdateScriptsConfiguration cfg) {
      this.updateScriptsConfiguration = cfg;
   }

   @Override
   public StoreUpdateScriptsConfiguration getUpdateScriptsConfiguration() {
      return updateScriptsConfiguration;
   }

   @Override
   public void setEventListeners(Map<String, Object> listeners) {
      this.eventListeners = listeners;
   }

   @Override
   public void setDataSource(DataSource datasource) {
      this.datasource = datasource;
   }

   @Override
   public void setCacheRegionFactory(Object cacheRegionFactory) {
      this.cacheRegionFactory = cacheRegionFactory;
   }

   /**
    * inject any bean properties you need - those properties will be used to build either session
    * factory or entity manager factory later.
    * 
    * @param beanProperties
    *           actual bean properties
    */
   public void setBeanProperties(Map<String, Object> beanProperties) {
      this.beanProperties = beanProperties;
   }
}
